'use strict';

import utils from '../utils.js';
import parseHeaders from '../helpers/parseHeaders.js';

const $internals = Symbol('internals');

function normalizeHeader(header) {
    return header && String(header).trim().toLowerCase();
}

function normalizeValue(value) {
    if (value === false || value == null) {
        return value;
    }

    return utils.isArray(value) ? value.map(normalizeValue) : String(value);
}

function parseTokens(str) {
    const tokens = Object.create(null);
    const tokensRE = /([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;
    let match;

    while ((match = tokensRE.exec(str))) {
        tokens[match[1]] = match[2];
    }

    return tokens;
}

const isValidHeaderName = (str) => /^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(str.trim());

function matchHeaderValue(context, value, header, filter, isHeaderNameFilter) {
    if (utils.isFunction(filter)) {
        return filter.call(this, value, header);
    }

    if (isHeaderNameFilter) {
        value = header;
    }

    if (!utils.isString(value)) return;

    if (utils.isString(filter)) {
        return value.indexOf(filter) !== -1;
    }

    if (utils.isRegExp(filter)) {
        return filter.test(value);
    }
}

function formatHeader(header) {
    return header.trim()
        .toLowerCase().replace(/([a-z\d])(\w*)/g, (w, char, str) => {
            return char.toUpperCase() + str;
        });
}

function buildAccessors(obj, header) {
    const accessorName = utils.toCamelCase(' ' + header);

    ['get', 'set', 'has'].forEach(methodName => {
        Object.defineProperty(obj, methodName + accessorName, {
            value: function (arg1, arg2, arg3) {
                return this[methodName].call(this, header, arg1, arg2, arg3);
            },
            configurable: true
        });
    });
}

class AxiosHeaders {
    constructor(headers) {
        headers && this.set(headers);
    }

    set(header, valueOrRewrite, rewrite) {
        const self = this;

        function setHeader(_value, _header, _rewrite) {
            const lHeader = normalizeHeader(_header);

            if (!lHeader) {
                throw new Error('header name must be a non-empty string');
            }

            const key = utils.findKey(self, lHeader);

            if (!key || self[key] === undefined || _rewrite === true || (_rewrite === undefined && self[key] !== false)) {
                self[key || _header] = normalizeValue(_value);
            }
        }

        const setHeaders = (headers, _rewrite) =>
            utils.forEach(headers, (_value, _header) => setHeader(_value, _header, _rewrite));

        if (utils.isPlainObject(header) || header instanceof this.constructor) {
            setHeaders(header, valueOrRewrite)
        } else if (utils.isString(header) && (header = header.trim()) && !isValidHeaderName(header)) {
            setHeaders(parseHeaders(header), valueOrRewrite);
        } else if (utils.isHeaders(header)) {
            for (const [key, value] of header.entries()) {
                setHeader(value, key, rewrite);
            }
        } else {
            header != null && setHeader(valueOrRewrite, header, rewrite);
        }

        return this;
    }

    get(header, parser) {
        header = normalizeHeader(header);

        if (header) {
            const key = utils.findKey(this, header);

            if (key) {
                const value = this[key];

                if (!parser) {
                    return value;
                }

                if (parser === true) {
                    return parseTokens(value);
                }

                if (utils.isFunction(parser)) {
                    return parser.call(this, value, key);
                }

                if (utils.isRegExp(parser)) {
                    return parser.exec(value);
                }

                throw new TypeError('parser must be boolean|regexp|function');
            }
        }
    }

    has(header, matcher) {
        header = normalizeHeader(header);

        if (header) {
            const key = utils.findKey(this, header);

            return !!(key && this[key] !== undefined && (!matcher || matchHeaderValue(this, this[key], key, matcher)));
        }

        return false;
    }

    delete(header, matcher) {
        const self = this;
        let deleted = false;

        function deleteHeader(_header) {
            _header = normalizeHeader(_header);

            if (_header) {
                const key = utils.findKey(self, _header);

                if (key && (!matcher || matchHeaderValue(self, self[key], key, matcher))) {
                    delete self[key];

                    deleted = true;
                }
            }
        }

        if (utils.isArray(header)) {
            header.forEach(deleteHeader);
        } else {
            deleteHeader(header);
        }

        return deleted;
    }

    clear(matcher) {
        const keys = Object.keys(this);
        let i = keys.length;
        let deleted = false;

        while (i--) {
            const key = keys[i];
            if (!matcher || matchHeaderValue(this, this[key], key, matcher, true)) {
                delete this[key];
                deleted = true;
            }
        }

        return deleted;
    }

    normalize(format) {
        const self = this;
        const headers = {};

        utils.forEach(this, (value, header) => {
            const key = utils.findKey(headers, header);

            if (key) {
                self[key] = normalizeValue(value);
                delete self[header];
                return;
            }

            const normalized = format ? formatHeader(header) : String(header).trim();

            if (normalized !== header) {
                delete self[header];
            }

            self[normalized] = normalizeValue(value);

            headers[normalized] = true;
        });

        return this;
    }

    concat(...targets) {
        return this.constructor.concat(this, ...targets);
    }

    toJSON(asStrings) {
        const obj = Object.create(null);

        utils.forEach(this, (value, header) => {
            value != null && value !== false && (obj[header] = asStrings && utils.isArray(value) ? value.join(', ') : value);
        });

        return obj;
    }

    [Symbol.iterator]() {
        return Object.entries(this.toJSON())[Symbol.iterator]();
    }

    toString() {
        return Object.entries(this.toJSON()).map(([header, value]) => header + ': ' + value).join('\n');
    }

    get [Symbol.toStringTag]() {
        return 'AxiosHeaders';
    }

    static from(thing) {
        return thing instanceof this ? thing : new this(thing);
    }

    static concat(first, ...targets) {
        const computed = new this(first);

        targets.forEach((target) => computed.set(target));

        return computed;
    }

    static accessor(header) {
        const internals = this[$internals] = (this[$internals] = {
            accessors: {}
        });

        const accessors = internals.accessors;
        const prototype = this.prototype;

        function defineAccessor(_header) {
            const lHeader = normalizeHeader(_header);

            if (!accessors[lHeader]) {
                buildAccessors(prototype, _header);
                accessors[lHeader] = true;
            }
        }

        utils.isArray(header) ? header.forEach(defineAccessor) : defineAccessor(header);

        return this;
    }
}

AxiosHeaders.accessor(['Content-Type', 'Content-Length', 'Accept', 'Accept-Encoding', 'User-Agent', 'Authorization']);

// reserved names hotfix
utils.reduceDescriptors(AxiosHeaders.prototype, ({value}, key) => {
    let mapped = key[0].toUpperCase() + key.slice(1); // map `set` => `Set`
    return {
        get: () => value,
        set(headerValue) {
            this[mapped] = headerValue;
        }
    }
});

utils.freezeMethods(AxiosHeaders);

export default AxiosHeaders;
